package nl.science.uva.pico;

import java.io.IOException;
import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import nl.science.uva.pico.operand.Operand;
import nl.science.uva.pico.operator.Add;
import nl.science.uva.pico.operator.Concat;
import nl.science.uva.pico.operator.Count;
import nl.science.uva.pico.operator.Div;
import nl.science.uva.pico.operator.Goto;
import nl.science.uva.pico.operator.IfEqual;
import nl.science.uva.pico.operator.IfLess;
import nl.science.uva.pico.operator.IfLessEqual;
import nl.science.uva.pico.operator.Load;
import nl.science.uva.pico.operator.Minus;
import nl.science.uva.pico.operator.Mult;
import nl.science.uva.pico.operator.Operator;
import nl.science.uva.pico.operator.Pop;
import nl.science.uva.pico.operator.Skip;
import nl.science.uva.pico.operator.Stop;
import nl.science.uva.pico.operator.Store;
import nl.uva.science.pico.stacklanguage.Factory;
import nl.uva.science.pico.stacklanguage.types.Instr;
import nl.uva.science.pico.stacklanguage.types.InstrList;
import nl.uva.science.pico.stacklanguage.types.LabelInstr;
import nl.uva.science.pico.stacklanguage.types.LabelInstrList;
import nl.uva.science.pico.stacklanguage.types.instr.Nat;
import nl.uva.science.pico.stacklanguage.types.instr.Str;
import aterm.pure.PureFactory;

/** 
 * Interpreter processes stack machine programs 
 * 
 * @author Ivo van Dongen, Karel Pieterson
 * 
 * @version 1.0
 */
public class Interpreter {
	private static Logger logger = LoggerFactory.getLogger(Interpreter.class);
	
	private Stack stack = new Stack();	
	private ProgramCounter programCounter;
	private LabelInstrList programListing;
	private LabelInstr currentInstr = null; 
	
	/**
	 * Parses the given {@link InputStream} and tries to interpret it.
	 * 
	 * @param inputStream the {@link InputStream} holding the stack program
	 * @return The result (contents of the stack after program termination)
	 * @throws IOException on read/parse error
	 */
	public String interpretStream(InputStream inputStream) throws IOException {
		//Initialize
		stack = new Stack();
		programCounter = new ProgramCounter();
		
		//Create our stack factory factory to parse the input
		PureFactory pureFactory = new PureFactory();
		Factory factory = Factory.getInstance(pureFactory);
		
		//Parse the input stream
		InstrList instrList = factory.InstrListFromFile(inputStream);
		
		//get the stack program
		programListing = (LabelInstrList) instrList.getChildAt(0);
		
		//Run the program
		return runInterpreter();
	}
	
	/** 
	 * Run the interpreter with the stack program
	 * 
	 * @return The result (contents of the stack after program termination)
     *  
     */		
	private String runInterpreter() {
		/*
		 * Process while not end of stackprogram or Stop operator encountered
		 */
				
		while(!(programCounter.getCurrentLine() >= programListing.getLength() || (currentInstr != null && currentInstr.getInstr().isStop()))) {
			//fetch current label and instruction
			currentInstr = programListing.getLabelInstrAt(programCounter.getCurrentLine());
			//set counter to next instruction
			programCounter.setCurrentLine(programCounter.getCurrentLine() + 1);
			
			//extract instruction from program line
			Instr instr = currentInstr.getInstr();
			
			//Do some debug logging
			if(logger.isDebugEnabled()) {
				logger.debug("Processing instruction: " + currentInstr + ". Stack: " + stack);
			}
			
			if(instr.isNat()) {
				//store Integer operand
				stack.push(new Operand(((Nat)instr).getNatCon()));
			} else if(instr.isStr()) {
				//store String operand
				stack.push(new Operand(((Str)instr).getStrCon()));
			} else {
				//process instruction
				Operator operator = createOperator(instr, stack, programCounter, programListing);
				try {
					operator.perform();
				} catch (Exception e) {
					// annotate exception and re-throw
					throw new RuntimeException(
							"Error performing instruction: " + currentInstr + 
							".\nState: \n\tStack: " + stack + "\n\t" + programCounter + ".\nError message: " + e.getMessage()
							, e
						);
				}
			}	
		}
		//end of interpreter life, return the stack.
		return stack.toString();
	}
	
	/** 
	 * determine operator and instantiate right one
     *  
     *  @param Instruction
     *  
     *  @throws IllegalArgumentException
     *  
     */		
	private static Operator createOperator(Instr instr, Stack stack, ProgramCounter programCounter, LabelInstrList programListing) {
		if (instr.isMinus()) {
			return new Minus(stack);
		} if (instr.isAdd()) {
			return new Add(stack);
		} if (instr.isMult()) {
			return new Mult(stack);
		} if (instr.isDiv()) {
			return new Div(stack);
		} if (instr.isConcat()) {
			return new Concat(stack);
		} if (instr.isIfequal()) {
			return new IfEqual(stack, programCounter, programListing);
		} if (instr.isIflessequal()) {
			return new IfLessEqual(stack, programCounter, programListing);
		} if (instr.isIfless()) {
			return new IfLess(stack, programCounter, programListing);
		} if (instr.isGoto()) {
			return new Goto(stack, programCounter, programListing);
		} if (instr.isPop()) {
			return new Pop(stack);
		} if (instr.isCount()) {
			return new Count(stack);
		} if (instr.isStore()) {
			return new Store(stack);
		} if (instr.isLoad()) {
			return new Load(stack);
		} if (instr.isSkip()) {
			return new Skip();
		} if (instr.isStop()) {
			return new Stop();
		}	
		else {
			throw new IllegalArgumentException("Unknown operator type");
		}
	}
}
